Fix 18 UX issues: confirmations, undo, drawer nav, empty states, and polish
Build and push image / build (push) Successful in 54s
Build and push image / build (push) Successful in 54s
Comprehensive UX audit covering modals, drawers, dashboard, and inventory. Key changes: confirmation steps before destructive actions, undo via toast for consume/gone/checkout, back-navigation across entity drawers, optional ratings, discrete item count field, audit progress bar, and sortable column affordance. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import type { Bootstrap, Item } from "../../types.js";
|
||||
import { helpers, enrichItems } from "../../types.js";
|
||||
@@ -24,18 +24,31 @@ export function ConsumeFlow({
|
||||
const allItems = enrichItems(data);
|
||||
const active = allItems.filter((i) => i.status === "active" || i.status === "checked-out");
|
||||
const [itemId, setItemId] = useState(initialItem?.id ?? "");
|
||||
const [rating, setRating] = useState(4);
|
||||
const [rating, setRating] = useState<number | null>(null);
|
||||
const [notes, setNotes] = useState("");
|
||||
const [date, setDate] = useState(getToday(getStoredTimezone()));
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [confirming, setConfirming] = useState(false);
|
||||
|
||||
const item = allItems.find((i) => i.id === itemId);
|
||||
|
||||
useEffect(() => { setError(null); setConfirming(false); }, [itemId]);
|
||||
|
||||
const finish = useMutation({
|
||||
mutationFn: () => api.finishInventoryItem(itemId, { date, rating, notes }),
|
||||
mutationFn: () => api.finishInventoryItem(itemId, { date, rating: rating ?? undefined, notes }),
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({ queryKey: ["bootstrap"] });
|
||||
toast(`Marked ${item?.name ?? "item"} as consumed — ${rating}/5 stars`);
|
||||
const ratingStr = rating != null ? ` — ${rating}/5 stars` : "";
|
||||
const undoItemId = itemId;
|
||||
const undoBinId = item?.binId ?? data.bins[0]?.id ?? "";
|
||||
toast(`Marked ${item?.name ?? "item"} as consumed${ratingStr}`, "success", {
|
||||
label: "Undo",
|
||||
onClick: () => {
|
||||
api.reactivateInventoryItem(undoItemId, { binId: undoBinId }).then(() => {
|
||||
qc.invalidateQueries({ queryKey: ["bootstrap"] });
|
||||
});
|
||||
},
|
||||
});
|
||||
onClose();
|
||||
},
|
||||
onError: (e: Error) => setError(e.message),
|
||||
@@ -110,7 +123,7 @@ export function ConsumeFlow({
|
||||
<Field label="Date finished">
|
||||
<Input type="date" value={date} onChange={(e) => setDate(e.target.value)} />
|
||||
</Field>
|
||||
<Field label="Rating">
|
||||
<Field label="Rating" hint="Optional — click to rate, click again to clear">
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
@@ -125,13 +138,13 @@ export function ConsumeFlow({
|
||||
{[1, 2, 3, 4, 5].map((n) => (
|
||||
<button
|
||||
key={n}
|
||||
onClick={() => setRating(n)}
|
||||
onClick={() => setRating(rating === n ? null : n)}
|
||||
style={{ border: "none", background: "transparent", cursor: "pointer", padding: 2 }}
|
||||
>
|
||||
<Icon
|
||||
name="star"
|
||||
size={20}
|
||||
color={n <= rating ? "var(--amber)" : "var(--ink-4)"}
|
||||
color={rating != null && n <= rating ? "var(--amber)" : "var(--ink-4)"}
|
||||
/>
|
||||
</button>
|
||||
))}
|
||||
@@ -143,7 +156,7 @@ export function ConsumeFlow({
|
||||
fontFamily: "var(--mono)",
|
||||
}}
|
||||
>
|
||||
{rating}/5
|
||||
{rating != null ? `${rating}/5` : "—"}
|
||||
</span>
|
||||
</div>
|
||||
</Field>
|
||||
@@ -160,6 +173,21 @@ export function ConsumeFlow({
|
||||
</>
|
||||
)}
|
||||
|
||||
{confirming && item && (
|
||||
<div
|
||||
style={{
|
||||
marginTop: 20,
|
||||
padding: 14,
|
||||
background: "var(--amber-soft)",
|
||||
border: "1px solid var(--amber)",
|
||||
borderRadius: "var(--r-md)",
|
||||
fontSize: 13,
|
||||
}}
|
||||
>
|
||||
Mark <strong>{item.name}</strong> (<span className="mono">{item.assetId}</span>) as consumed? This cannot be undone.
|
||||
</div>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<div style={{ marginTop: 14, fontSize: 12, color: "var(--terracotta)" }}>{error}</div>
|
||||
)}
|
||||
@@ -171,14 +199,28 @@ export function ConsumeFlow({
|
||||
</div>
|
||||
<div style={{ display: "flex", gap: 8 }}>
|
||||
<Btn variant="ghost" onClick={onClose}>Cancel</Btn>
|
||||
<Btn
|
||||
variant="primary"
|
||||
icon="check"
|
||||
disabled={finish.isPending || !item}
|
||||
onClick={() => finish.mutate()}
|
||||
>
|
||||
{finish.isPending ? "Saving…" : error ? "Try again" : "Mark consumed"}
|
||||
</Btn>
|
||||
{confirming ? (
|
||||
<>
|
||||
<Btn variant="ghost" onClick={() => setConfirming(false)}>Back</Btn>
|
||||
<Btn
|
||||
variant="danger"
|
||||
icon="check"
|
||||
disabled={finish.isPending}
|
||||
onClick={() => finish.mutate()}
|
||||
>
|
||||
{finish.isPending ? "Saving…" : "Confirm"}
|
||||
</Btn>
|
||||
</>
|
||||
) : (
|
||||
<Btn
|
||||
variant="primary"
|
||||
icon="check"
|
||||
disabled={!item}
|
||||
onClick={() => setConfirming(true)}
|
||||
>
|
||||
{error ? "Try again" : "Mark consumed"}
|
||||
</Btn>
|
||||
)}
|
||||
</div>
|
||||
</ModalFooter>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user