toggle update
This commit is contained in:
@@ -31,27 +31,30 @@ import './pages/deliveries.css';
|
|||||||
export function HealthScorePanel({
|
export function HealthScorePanel({
|
||||||
product,
|
product,
|
||||||
category,
|
category,
|
||||||
onToggleShown,
|
|
||||||
showToggle,
|
showToggle,
|
||||||
onScoreResolved,
|
onScoreResolved,
|
||||||
}: {
|
}: {
|
||||||
product: Product;
|
product: Product;
|
||||||
/**
|
/**
|
||||||
* Turn the score on or off for a product ALREADY on the shelf. Omitted where
|
* The show/hide control, for both moments it appears in: a product being
|
||||||
* the viewer does not get to decide — the Nearle staff console passes
|
* imported, where the answer is carried into the import, and one already on
|
||||||
* nothing, because the decision being made there is whether the data is fit
|
* the shelf, where each click is a write.
|
||||||
* to publish at all, not whether one shopkeeper wants it on their shelf.
|
|
||||||
*/
|
|
||||||
onToggleShown?: (show: boolean) => void;
|
|
||||||
/**
|
|
||||||
* The same control, for a product being imported — where the answer is not
|
|
||||||
* saved on each click but carried into the import.
|
|
||||||
*
|
*
|
||||||
* Separate from `onToggleShown` because the two are different moments with
|
* One prop rather than two, because the difference is entirely the caller's —
|
||||||
* different state: one writes immediately, the other is a form field until
|
* the panel renders the same switch either way, and two props meant two code
|
||||||
* the import button is pressed.
|
* paths that could drift in what they looked like.
|
||||||
|
*
|
||||||
|
* Omitted where the viewer does not get to decide: the Nearle staff console
|
||||||
|
* passes nothing, and so does a branch user, since this writes per tenant.
|
||||||
*/
|
*/
|
||||||
showToggle?: { isShown: boolean; onChange: (show: boolean) => void };
|
showToggle?: {
|
||||||
|
isShown: boolean;
|
||||||
|
onChange: (show: boolean) => void;
|
||||||
|
/** Shown beneath the switch. A write that fails silently is the bug this
|
||||||
|
panel already shipped once. */
|
||||||
|
error?: string | undefined;
|
||||||
|
isBusy?: boolean | undefined;
|
||||||
|
};
|
||||||
/**
|
/**
|
||||||
* Reports whether this product has a score at all.
|
* Reports whether this product has a score at all.
|
||||||
*
|
*
|
||||||
@@ -188,9 +191,7 @@ export function HealthScorePanel({
|
|||||||
<Note>
|
<Note>
|
||||||
Hidden from customers for this product. The nutrition figures are still shown.
|
Hidden from customers for this product. The nutrition figures are still shown.
|
||||||
</Note>
|
</Note>
|
||||||
{onToggleShown ? (
|
{showToggle ? <ShowScoreToggle {...showToggle} /> : null}
|
||||||
<ShowScoreToggle isShown={false} onChange={onToggleShown} />
|
|
||||||
) : null}
|
|
||||||
</Section>
|
</Section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -313,11 +314,7 @@ export function HealthScorePanel({
|
|||||||
{/* Beneath the score, deliberately. The decision is about the thing above
|
{/* Beneath the score, deliberately. The decision is about the thing above
|
||||||
it, and a shopkeeper weighing "is this rating fair to my product"
|
it, and a shopkeeper weighing "is this rating fair to my product"
|
||||||
should be reading the rating and the confidence while they decide. */}
|
should be reading the rating and the confidence while they decide. */}
|
||||||
{showToggle ? (
|
{showToggle ? <ShowScoreToggle {...showToggle} /> : null}
|
||||||
<ShowScoreToggle isShown={showToggle.isShown} onChange={showToggle.onChange} />
|
|
||||||
) : onToggleShown ? (
|
|
||||||
<ShowScoreToggle isShown onChange={onToggleShown} />
|
|
||||||
) : null}
|
|
||||||
</Section>
|
</Section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -338,9 +335,13 @@ export function HealthScorePanel({
|
|||||||
function ShowScoreToggle({
|
function ShowScoreToggle({
|
||||||
isShown,
|
isShown,
|
||||||
onChange,
|
onChange,
|
||||||
|
error,
|
||||||
|
isBusy,
|
||||||
}: {
|
}: {
|
||||||
isShown: boolean;
|
isShown: boolean;
|
||||||
onChange: (show: boolean) => void;
|
onChange: (show: boolean) => void;
|
||||||
|
error?: string | undefined;
|
||||||
|
isBusy?: boolean | undefined;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div style={{ marginTop: 12 }}>
|
<div style={{ marginTop: 12 }}>
|
||||||
@@ -349,9 +350,18 @@ function ShowScoreToggle({
|
|||||||
description="Appears on this product in your catalogue and in the customer app. The nutrition figures are shown either way."
|
description="Appears on this product in your catalogue and in the customer app. The nutrition figures are shown either way."
|
||||||
value={isShown}
|
value={isShown}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
|
isLoading={Boolean(isBusy)}
|
||||||
size="sm"
|
size="sm"
|
||||||
labelSpacing="spread"
|
labelSpacing="spread"
|
||||||
/>
|
/>
|
||||||
|
{/* A write that fails has to say so. This one shipped without it: the
|
||||||
|
switch moved, nothing saved, and the only way to find out was to call
|
||||||
|
the API by hand. */}
|
||||||
|
{error ? (
|
||||||
|
<p style={{ margin: '6px 0 0', fontSize: 11.5, color: 'var(--color-error, #d64545)' }}>
|
||||||
|
{error}
|
||||||
|
</p>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useMemo, useState } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
import { BookOpen, ClipboardList, EyeOff, Package, Ruler, Tag } from 'lucide-react';
|
import { BookOpen, ClipboardList, EyeOff, Package, Ruler, Tag } from 'lucide-react';
|
||||||
|
import { errorMessage } from '@/api/client';
|
||||||
import { productsApi } from '@/api/products';
|
import { productsApi } from '@/api/products';
|
||||||
import type { CatalogueProduct, Product, StockRequest } from '@/api/types';
|
import type { CatalogueProduct, Product, StockRequest } from '@/api/types';
|
||||||
/* A pure reading of a `StockRequest`, not a Store user component — the same
|
/* A pure reading of a `StockRequest`, not a Store user component — the same
|
||||||
@@ -141,9 +142,36 @@ export function ProductDrawer({
|
|||||||
stale would mean the drawer and the line behind it disagreeing about what a
|
stale would mean the drawer and the line behind it disagreeing about what a
|
||||||
customer sees.
|
customer sees.
|
||||||
*/
|
*/
|
||||||
|
/*
|
||||||
|
What this drawer believes the switch is set to.
|
||||||
|
|
||||||
|
Local, because `product` is a prop captured when the drawer opened and does
|
||||||
|
not change when the list behind it refetches — so without this the switch
|
||||||
|
read the same value before and after a click, and a save that worked looked
|
||||||
|
exactly like one that did not. It shipped that way once.
|
||||||
|
|
||||||
|
Seeded from the record and reconciled on failure, never on success: the
|
||||||
|
server has agreed by then, and snapping back to a stale prop would undo what
|
||||||
|
the shopkeeper just did on screen.
|
||||||
|
*/
|
||||||
|
const [scoreShown, setScoreShown] = useState(product.showhealthscore !== false);
|
||||||
|
const [scoreError, setScoreError] = useState<string | null>(null);
|
||||||
|
|
||||||
const showScore = useMutation({
|
const showScore = useMutation({
|
||||||
mutationFn: (show: boolean) =>
|
mutationFn: (show: boolean) =>
|
||||||
productsApi.setShowHealthScore(tenantid as number, product.productid, show),
|
productsApi.setShowHealthScore(tenantid as number, product.productid, show),
|
||||||
|
onMutate: (show: boolean) => {
|
||||||
|
setScoreError(null);
|
||||||
|
setScoreShown(show);
|
||||||
|
return { previous: !show };
|
||||||
|
},
|
||||||
|
onError: (error, _show, context) => {
|
||||||
|
// Put the switch back where it was and say why. A write that fails
|
||||||
|
// silently is worse than one that fails loudly: the shopkeeper walks away
|
||||||
|
// believing a rating is hidden from their customers when it is not.
|
||||||
|
if (context) setScoreShown(context.previous);
|
||||||
|
setScoreError(errorMessage(error));
|
||||||
|
},
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
void client.invalidateQueries({ queryKey: queryKeys.products.all });
|
void client.invalidateQueries({ queryKey: queryKeys.products.all });
|
||||||
},
|
},
|
||||||
@@ -271,7 +299,14 @@ export function ProductDrawer({
|
|||||||
product={product}
|
product={product}
|
||||||
category={product.categoryname || product.Subcategoryname}
|
category={product.categoryname || product.Subcategoryname}
|
||||||
{...(canManage && product.tenantid && product.productid
|
{...(canManage && product.tenantid && product.productid
|
||||||
? { onToggleShown: (show: boolean) => showScore.mutate(show) }
|
? {
|
||||||
|
showToggle: {
|
||||||
|
isShown: scoreShown,
|
||||||
|
onChange: (show: boolean) => showScore.mutate(show),
|
||||||
|
error: scoreError ?? undefined,
|
||||||
|
isBusy: showScore.isPending,
|
||||||
|
},
|
||||||
|
}
|
||||||
: {})}
|
: {})}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user