|
|
@@ -25,10 +25,14 @@ import {
|
|
|
import { detailPageRouteLoader } from '@/vdb/framework/page/detail-page-route-loader.js';
|
|
|
import { useDetailPage } from '@/vdb/framework/page/use-detail-page.js';
|
|
|
import { useChannel } from '@/vdb/hooks/use-channel.js';
|
|
|
+import { useLocalFormat } from '@/vdb/hooks/use-local-format.js';
|
|
|
import { Trans, useLingui } from '@lingui/react/macro';
|
|
|
import { createFileRoute, useNavigate } from '@tanstack/react-router';
|
|
|
+import { VariablesOf } from 'gql.tada';
|
|
|
+import { Trash } from 'lucide-react';
|
|
|
import { Fragment } from 'react/jsx-runtime';
|
|
|
import { toast } from 'sonner';
|
|
|
+import { AddCurrencyDropdown } from './components/add-currency-dropdown.js';
|
|
|
import { VariantPriceDetail } from './components/variant-price-detail.js';
|
|
|
import {
|
|
|
createProductVariantDocument,
|
|
|
@@ -57,12 +61,15 @@ export const Route = createFileRoute('/_authenticated/_product-variants/product-
|
|
|
errorComponent: ({ error }) => <ErrorPage message={error.message} />,
|
|
|
});
|
|
|
|
|
|
+type PriceInput = NonNullable<VariablesOf<typeof updateProductVariantDocument>['input']['prices']>[number];
|
|
|
+
|
|
|
function ProductVariantDetailPage() {
|
|
|
const params = Route.useParams();
|
|
|
const navigate = useNavigate();
|
|
|
const creatingNewEntity = params.id === NEW_ENTITY_PATH;
|
|
|
const { t } = useLingui();
|
|
|
const { activeChannel } = useChannel();
|
|
|
+ const { formatCurrencyName } = useLocalFormat();
|
|
|
|
|
|
const { form, submitHandler, entity, isPending, resetForm } = useDetailPage({
|
|
|
pageId,
|
|
|
@@ -79,7 +86,7 @@ function ProductVariantDetailPage() {
|
|
|
facetValueIds: entity.facetValues.map(facetValue => facetValue.id),
|
|
|
taxCategoryId: entity.taxCategory.id,
|
|
|
price: entity.price,
|
|
|
- prices: [],
|
|
|
+ prices: entity.prices,
|
|
|
trackInventory: entity.trackInventory,
|
|
|
outOfStockThreshold: entity.outOfStockThreshold,
|
|
|
stockLevels: entity.stockLevels.map(stockLevel => ({
|
|
|
@@ -117,7 +124,59 @@ function ProductVariantDetailPage() {
|
|
|
},
|
|
|
});
|
|
|
|
|
|
- const [price, taxCategoryId] = form.watch(['price', 'taxCategoryId']);
|
|
|
+ const availableCurrencies = activeChannel?.availableCurrencyCodes ?? [];
|
|
|
+ const [prices, taxCategoryId] = form.watch(['prices', 'taxCategoryId']);
|
|
|
+
|
|
|
+ // Filter out deleted prices for display
|
|
|
+ const activePrices = prices?.filter(p => !p.delete) ?? [];
|
|
|
+
|
|
|
+ // Get currencies that are currently active (not deleted)
|
|
|
+ const usedCurrencies = activePrices.map(p => p.currencyCode);
|
|
|
+ const unusedCurrencies = availableCurrencies.filter(c => !usedCurrencies.includes(c));
|
|
|
+
|
|
|
+ const handleAddCurrency = (currencyCode: string) => {
|
|
|
+ const currentPrices = form.getValues('prices') || [];
|
|
|
+
|
|
|
+ // Check if this currency already exists (including deleted ones)
|
|
|
+ const existingPriceIndex = currentPrices.findIndex(p => p.currencyCode === currencyCode);
|
|
|
+
|
|
|
+ if (existingPriceIndex !== -1) {
|
|
|
+ // Currency exists, mark it as not deleted
|
|
|
+ const updatedPrices = [...currentPrices];
|
|
|
+ updatedPrices[existingPriceIndex] = {
|
|
|
+ ...updatedPrices[existingPriceIndex],
|
|
|
+ delete: false,
|
|
|
+ };
|
|
|
+ form.setValue('prices', updatedPrices, {
|
|
|
+ shouldDirty: true,
|
|
|
+ shouldValidate: true,
|
|
|
+ });
|
|
|
+ } else {
|
|
|
+ // Add new currency
|
|
|
+ const newPrice = {
|
|
|
+ currencyCode,
|
|
|
+ price: 0,
|
|
|
+ delete: false,
|
|
|
+ } as PriceInput;
|
|
|
+ form.setValue('prices', [...currentPrices, newPrice], {
|
|
|
+ shouldDirty: true,
|
|
|
+ shouldValidate: true,
|
|
|
+ });
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ const handleRemoveCurrency = (indexToRemove: number) => {
|
|
|
+ const currentPrices = form.getValues('prices') || [];
|
|
|
+ const updatedPrices = [...currentPrices];
|
|
|
+ updatedPrices[indexToRemove] = {
|
|
|
+ ...updatedPrices[indexToRemove],
|
|
|
+ delete: true,
|
|
|
+ };
|
|
|
+ form.setValue('prices', updatedPrices, {
|
|
|
+ shouldDirty: true,
|
|
|
+ shouldValidate: true,
|
|
|
+ });
|
|
|
+ };
|
|
|
|
|
|
return (
|
|
|
<Page pageId={pageId} form={form} submitHandler={submitHandler} entity={entity}>
|
|
|
@@ -168,7 +227,7 @@ function ProductVariantDetailPage() {
|
|
|
<CustomFieldsPageBlock column="main" entityType="ProductVariant" control={form.control} />
|
|
|
|
|
|
<PageBlock column="main" blockId="price-and-tax" title={<Trans>Price and tax</Trans>}>
|
|
|
- <div className="grid grid-cols-2 gap-4 items-start">
|
|
|
+ <DetailFormGrid>
|
|
|
<FormFieldWrapper
|
|
|
control={form.control}
|
|
|
name="taxCategoryId"
|
|
|
@@ -177,26 +236,59 @@ function ProductVariantDetailPage() {
|
|
|
<TaxCategorySelector value={field.value} onChange={field.onChange} />
|
|
|
)}
|
|
|
/>
|
|
|
+ </DetailFormGrid>
|
|
|
+ {activePrices.map((price, displayIndex) => {
|
|
|
+ // Find the actual index in the full prices array
|
|
|
+ const actualIndex = prices?.findIndex(p => p === price) ?? displayIndex;
|
|
|
|
|
|
- <div>
|
|
|
- <FormFieldWrapper
|
|
|
- control={form.control}
|
|
|
- name="price"
|
|
|
- label={<Trans>Price</Trans>}
|
|
|
- render={({ field }) => (
|
|
|
- <MoneyInput {...field} currency={entity?.currencyCode} />
|
|
|
- )}
|
|
|
- />
|
|
|
- <VariantPriceDetail
|
|
|
- priceIncludesTax={activeChannel?.pricesIncludeTax ?? false}
|
|
|
- price={price}
|
|
|
- currencyCode={
|
|
|
- entity?.currencyCode ?? activeChannel?.defaultCurrencyCode ?? ''
|
|
|
- }
|
|
|
- taxCategoryId={taxCategoryId}
|
|
|
- />
|
|
|
- </div>
|
|
|
- </div>
|
|
|
+ const currencyCodeLabel = (
|
|
|
+ <div className="uppercase text-muted-foreground">{price.currencyCode}</div>
|
|
|
+ );
|
|
|
+ const priceLabel = (
|
|
|
+ <div className="flex gap-1 items-center justify-between">
|
|
|
+ <Trans>Price</Trans> {activePrices.length > 1 ? currencyCodeLabel : null}
|
|
|
+ </div>
|
|
|
+ );
|
|
|
+ return (
|
|
|
+ <DetailFormGrid key={price.currencyCode}>
|
|
|
+ <div className="flex gap-1 items-end">
|
|
|
+ <FormFieldWrapper
|
|
|
+ control={form.control}
|
|
|
+ name={`prices.${actualIndex}.price`}
|
|
|
+ label={priceLabel}
|
|
|
+ render={({ field }) => (
|
|
|
+ <MoneyInput {...field} currency={price.currencyCode} />
|
|
|
+ )}
|
|
|
+ />
|
|
|
+ {activePrices.length > 1 && (
|
|
|
+ <Button
|
|
|
+ type="button"
|
|
|
+ variant="ghost"
|
|
|
+ size="sm"
|
|
|
+ onClick={() => handleRemoveCurrency(actualIndex)}
|
|
|
+ className="h-6 w-6 p-0 mb-2 hover:text-destructive hover:bg-destructive-100"
|
|
|
+ >
|
|
|
+ <Trash className="size-4" />
|
|
|
+ </Button>
|
|
|
+ )}
|
|
|
+ </div>
|
|
|
+ <VariantPriceDetail
|
|
|
+ priceIncludesTax={activeChannel?.pricesIncludeTax ?? false}
|
|
|
+ price={price.price}
|
|
|
+ currencyCode={
|
|
|
+ price.currencyCode ?? activeChannel?.defaultCurrencyCode ?? ''
|
|
|
+ }
|
|
|
+ taxCategoryId={taxCategoryId}
|
|
|
+ />
|
|
|
+ </DetailFormGrid>
|
|
|
+ );
|
|
|
+ })}
|
|
|
+ {unusedCurrencies.length ? (
|
|
|
+ <AddCurrencyDropdown
|
|
|
+ onCurrencySelect={handleAddCurrency}
|
|
|
+ unusedCurrencies={unusedCurrencies}
|
|
|
+ />
|
|
|
+ ) : null}
|
|
|
</PageBlock>
|
|
|
<PageBlock column="main" blockId="stock" title={<Trans>Stock</Trans>}>
|
|
|
<DetailFormGrid>
|