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:
2026-03-24 15:54:22 +03:00
parent 669c4a3023
commit aa0cfe35c3
7 changed files with 315 additions and 75 deletions

View File

@@ -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">