fix: comprehensive bookings admin UX improvements
- #1 Delete confirmation dialog before removing bookings - #2 Error toasts instead of silent .catch(() => {}) - #3 Optimistic rollback — UI reverts on API failure - #4 Loading indicator on reminder status buttons - #5 Search results are now actionable (status change + delete) - #6 New bookings banner instead of full tab remount - #7 Error states for failed data loads - #8 InlineNotes only saves on blur when value changed - #9 AddBookingModal supports Instagram/Telegram fields - #10 Polling pauses when browser tab is hidden - #11 Enter key submits ConfirmModal
This commit is contained in:
@@ -7,6 +7,7 @@ import { type BookingStatus, type BookingFilter, type BaseBooking, type BookingG
|
||||
import { FilterTabs, EmptyState, BookingCard, ContactLinks, StatusBadge, StatusActions, DeleteBtn } from "./BookingComponents";
|
||||
import { fmtDate } from "./types";
|
||||
import { InlineNotes } from "./InlineNotes";
|
||||
import { useToast } from "./Toast";
|
||||
|
||||
interface GenericBookingsListProps<T extends BaseBooking> {
|
||||
items: T[];
|
||||
@@ -28,6 +29,7 @@ export function GenericBookingsList<T extends BaseBooking>({
|
||||
const [filter, setFilter] = useState<BookingFilter>("all");
|
||||
const [showArchived, setShowArchived] = useState(false);
|
||||
const [expanded, setExpanded] = useState<Record<string, boolean>>({});
|
||||
const { showError } = useToast();
|
||||
|
||||
const counts = useMemo(() => countStatuses(items), [items]);
|
||||
|
||||
@@ -36,26 +38,47 @@ export function GenericBookingsList<T extends BaseBooking>({
|
||||
onConfirm(id);
|
||||
return;
|
||||
}
|
||||
onItemsChange((prev) => prev.map((b) => b.id === id ? { ...b, status } : b));
|
||||
await adminFetch(endpoint, {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ action: "set-status", id, status }),
|
||||
});
|
||||
const prev = items.find((b) => b.id === id);
|
||||
const prevStatus = prev?.status;
|
||||
onItemsChange((list) => list.map((b) => b.id === id ? { ...b, status } : b));
|
||||
try {
|
||||
const res = await adminFetch(endpoint, {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ action: "set-status", id, status }),
|
||||
});
|
||||
if (!res.ok) throw new Error();
|
||||
} catch {
|
||||
if (prevStatus) onItemsChange((list) => list.map((b) => b.id === id ? { ...b, status: prevStatus } : b));
|
||||
showError("Не удалось обновить статус");
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDelete(id: number) {
|
||||
await adminFetch(`${endpoint}?id=${id}`, { method: "DELETE" });
|
||||
onItemsChange((prev) => prev.filter((b) => b.id !== id));
|
||||
try {
|
||||
const res = await adminFetch(`${endpoint}?id=${id}`, { method: "DELETE" });
|
||||
if (!res.ok) throw new Error();
|
||||
onItemsChange((list) => list.filter((b) => b.id !== id));
|
||||
} catch {
|
||||
showError("Не удалось удалить запись");
|
||||
}
|
||||
}
|
||||
|
||||
async function handleNotes(id: number, notes: string) {
|
||||
onItemsChange((prev) => prev.map((b) => b.id === id ? { ...b, notes: notes || undefined } : b));
|
||||
await adminFetch(endpoint, {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ action: "set-notes", id, notes }),
|
||||
});
|
||||
const prev = items.find((b) => b.id === id);
|
||||
const prevNotes = prev?.notes;
|
||||
onItemsChange((list) => list.map((b) => b.id === id ? { ...b, notes: notes || undefined } : b));
|
||||
try {
|
||||
const res = await adminFetch(endpoint, {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ action: "set-notes", id, notes }),
|
||||
});
|
||||
if (!res.ok) throw new Error();
|
||||
} catch {
|
||||
onItemsChange((list) => list.map((b) => b.id === id ? { ...b, notes: prevNotes } : b));
|
||||
showError("Не удалось сохранить заметку");
|
||||
}
|
||||
}
|
||||
|
||||
function renderItem(item: T, isArchived: boolean) {
|
||||
@@ -69,7 +92,7 @@ export function GenericBookingsList<T extends BaseBooking>({
|
||||
</div>
|
||||
<div className="flex items-center gap-2 shrink-0">
|
||||
<span className="text-neutral-600 text-xs">{fmtDate(item.createdAt)}</span>
|
||||
<DeleteBtn onClick={() => handleDelete(item.id)} />
|
||||
<DeleteBtn onClick={() => handleDelete(item.id)} name={item.name} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 mt-2 flex-wrap">
|
||||
|
||||
Reference in New Issue
Block a user