+
{entries.map(([pid, price], index) => (
k).filter(k => k !== pid)}
- onSave={(newId, newPrice) => {
- const finalId = newId || pid;
- setDraft(prev => {
- if (newPrice === 'include-by-default') {
- return { ...prev, prices: 'include-by-default' };
- }
- const prevPrices: PricesObject = getPricesObject(prev);
- const nextPrices: PricesObject = { ...prevPrices };
- if (newId && newId !== pid) {
- if (Object.prototype.hasOwnProperty.call(nextPrices, newId)) {
- toast({ title: "Price ID already exists" });
- return prev; // Do not change state
- }
- delete nextPrices[pid];
- }
- nextPrices[finalId] = newPrice;
- return { ...prev, prices: nextPrices };
- });
- if (editingPriceId && finalId === editingPriceId) {
- setEditingPriceId(undefined);
- }
- }}
- onRemove={() => handleRemovePrice(pid)}
+ onSave={() => { /* no-op in view mode */ }}
+ onRemove={() => { /* no-op in view mode */ }}
/>
- {((mode !== "view" && !editingPricesIsFreeMode) || index < entries.length - 1) && }
+ {index < entries.length - 1 && }
))}
);
};
- const itemsList = Object.entries(draft.includedItems);
+ const itemsList = Object.entries(product.includedItems);
- const couldBeAddOnTo = allProducts.filter(o => o.product.productLineId === draft.productLineId && o.id !== id);
- const isAddOnTo = allProducts.filter(o => draft.isAddOnTo && o.id in draft.isAddOnTo);
+ const isAddOnTo = allProducts.filter(o => product.isAddOnTo && o.id in product.isAddOnTo);
const PRODUCT_TOGGLE_OPTIONS = [{
key: 'serverOnly' as const,
label: 'Server only',
shortLabel: 'Server only',
description: "Restricts this product to only be purchased from server-side calls. Use this for backend-initiated purchases.",
- active: !!draft.serverOnly,
+ active: !!product.serverOnly,
visible: true,
icon:
,
- onToggle: () => setDraft(prev => ({ ...prev, serverOnly: !prev.serverOnly })),
- wrapButton: (button: ReactNode) => button,
}, {
key: 'stackable' as const,
label: 'Stackable',
shortLabel: 'Stackable',
description: "Allows customers to purchase this product multiple times. Each purchase adds to their existing quantity.",
- active: !!draft.stackable,
+ active: !!product.stackable,
visible: true,
icon:
,
- onToggle: () => setDraft(prev => ({ ...prev, stackable: !prev.stackable })),
- wrapButton: (button: ReactNode) => button,
}, {
key: 'addon' as const,
label: 'Add-on',
shortLabel: 'Add-on',
description: "Makes this an optional extra that customers can purchase alongside a main product.",
- visible: draft.isAddOnTo !== false || couldBeAddOnTo.length > 0,
- active: draft.isAddOnTo !== false,
+ visible: product.isAddOnTo !== false,
+ active: product.isAddOnTo !== false,
icon:
,
- onToggle: isAddOnTo.length === 0 && draft.isAddOnTo !== false ? () => setDraft(prev => ({ ...prev, isAddOnTo: false })) : undefined,
- wrapButton: (button: ReactNode) => isAddOnTo.length === 0 && draft.isAddOnTo !== false ? button : (
-
-
- {button}
-
-
- {couldBeAddOnTo.map(product => (
- o.id === product.id)}
- key={product.id}
- onCheckedChange={(checked) => setDraft(prev => {
- const newIsAddOnTo = { ...prev.isAddOnTo || {} };
- if (checked) {
- newIsAddOnTo[product.id] = true;
- } else {
- delete newIsAddOnTo[product.id];
- }
- return { ...prev, isAddOnTo: Object.keys(newIsAddOnTo).length > 0 ? newIsAddOnTo : false };
- })}
- className="cursor-pointer"
- >
- {product.product.displayName} ({product.id})
-
- ))}
-
-
- ),
}] as const;
- const handleCancelEdit = () => {
- if (isDraft && onCancelDraft) {
- onCancelDraft();
- return;
- }
- setIsEditing(false);
- setDraft(product);
- setLocalProductId(id);
- setEditingPriceId(undefined);
- };
-
- const handleSaveEdit = async () => {
- const trimmed = localProductId.trim();
- const validId = trimmed && isValidUserSpecifiedId(trimmed) ? trimmed : id;
- try {
- if (validId !== id) {
- await onSave(validId, draft);
- await onDelete(id);
- } else {
- await onSave(id, draft);
- }
- setIsEditing(false);
- setEditingPriceId(undefined);
- } catch (e) {
- // Validation error - don't close edit mode
- if (e instanceof ValidationError) {
- return;
- }
- throw e;
- }
- };
-
- const renderToggleButtons = (mode: 'editing' | 'view') => {
- const getLabel = (b: typeof PRODUCT_TOGGLE_OPTIONS[number], editing: boolean) => {
+ const renderToggleButtons = () => {
+ const getLabel = (b: typeof PRODUCT_TOGGLE_OPTIONS[number]) => {
if (b.key === "addon" && isAddOnTo.length > 0) {
return
Add-on to {isAddOnTo.map((o, i) => (
{i > 0 && ", "}
- {editing ? o.product.displayName : (
-
- {o.product.displayName}
-
- )}
+
+ {o.product.displayName}
+
))}
;
}
return b.shortLabel;
};
- return mode === 'editing' ? (
- PRODUCT_TOGGLE_OPTIONS
- .filter(b => b.visible !== false)
- .map((b) => {
- const wrap = b.wrapButton;
- return (
-
- {wrap(
-
- )}
-
- );
- })
- ) : (
- PRODUCT_TOGGLE_OPTIONS
- .filter(b => b.visible !== false)
- .filter(b => b.active)
- .map((b) => {
- return (
-
-
- {b.icon}
- {getLabel(b, false)}
-
-
- );
- })
- );
+ return PRODUCT_TOGGLE_OPTIONS
+ .filter(b => b.visible !== false)
+ .filter(b => b.active)
+ .map((b) => (
+
+
+ {b.icon}
+ {getLabel(b)}
+
+
+ ));
};
- const editingContent = (
-
- {/* Header */}
-
-
- {isDraft ? "New product" : "Edit product"}
-
-
-
- {/* Content */}
-
-
- {/* Name, ID & Type Fields */}
-
-
-
- Offer Name
-
- {
- const value = event.target.value;
- setDraft(prev => ({ ...prev, displayName: value }));
- }}
- placeholder="e.g., Pro Plan"
- />
-
-
-
- Offer ID
-
-
- {
- const value = event.target.value.toLowerCase().replace(/[^a-z0-9_\-]/g, '-');
- setLocalProductId(value);
- }}
- placeholder="e.g., pro-plan"
- disabled={!isDraft}
- />
-
-
-
-
- Customer Type
-
-
-
-
-
-
-
- {(['user', 'team', 'custom'] as const).map((type) => {
- const isSelected = draft.customerType === type;
- const descriptions = {
- user: 'For individual users',
- team: 'For teams or organizations',
- custom: 'Server-side managed customers',
- };
- return (
-
- );
- })}
-
-
-
-
-
-
- {/* Toggle Options */}
-
- {renderToggleButtons('editing')}
-
-
- {/* Prices Section */}
-
-
- {renderPrimaryPrices('editing')}
- {!editingPricesIsFreeMode && (
-
-
- {!hasExistingPrices && (
- <>
-
or
-
- >
- )}
-
- )}
-
-
- {/* Includes Section */}
-
- {itemsList.length === 0 ? (
-
- No items yet
-
- ) : (
-
- {itemsList.map(([itemId, item]) => {
- const itemMeta = existingItems.find(i => i.id === itemId);
- const itemLabel = itemMeta ? itemMeta.displayName : 'Select item';
- return (
-
id !== itemId)}
- startEditing={true}
- readOnly={false}
- onSave={(id, updated) => handleAddOrEditIncludedItem(id, updated)}
- onChangeItemId={(newItemId) => {
- setDraft(prev => {
- if (Object.prototype.hasOwnProperty.call(prev.includedItems, newItemId)) {
- toast({ title: "Item already included" });
- return prev;
- }
- const next: Product['includedItems'] = { ...prev.includedItems };
- const value = next[itemId];
- delete next[itemId];
- next[newItemId] = value;
- return { ...prev, includedItems: next };
- });
- }}
- onRemove={() => handleRemoveIncludedItem(itemId)}
- onCreateNewItem={onCreateNewItem}
- />
- );
- })}
-
- )}
-
-
-
-
- {/* Footer */}
-
-
-
-
-
-
-
-
-
-
- );
-
const handleCardClick = (e: React.MouseEvent) => {
// Don't navigate if clicking on interactive elements
const target = e.target as HTMLElement;
@@ -1329,15 +939,31 @@ function ProductCard({ id, product, allProducts, existingItems, onSave, onDelete
{customerType}
- ID: {localProductId}
+ ID: {id}
{/* Product name */}