Add deferred save for prize reordering in admin

- Remove auto-save on drag-and-drop (prevents UI jumps)
- Store prize order changes locally
- Show warning banner with unsaved changes
- Add Save and Cancel buttons
- Only trigger API call when user clicks Save
- Add translations for unsaved order messages
This commit is contained in:
c0mrade
2026-02-03 23:42:59 +03:00
parent 4fa9d7f47c
commit f6feb592d2

View File

@@ -277,6 +277,8 @@ export default function AdminWheel() {
const [activeTab, setActiveTab] = useState<Tab>('settings'); const [activeTab, setActiveTab] = useState<Tab>('settings');
const [expandedPrizeId, setExpandedPrizeId] = useState<number | null>(null); const [expandedPrizeId, setExpandedPrizeId] = useState<number | null>(null);
const [isCreating, setIsCreating] = useState(false); const [isCreating, setIsCreating] = useState(false);
const [localPrizeOrder, setLocalPrizeOrder] = useState<number[] | null>(null);
const [hasUnsavedOrder, setHasUnsavedOrder] = useState(false);
// Settings form state // Settings form state
const [settingsForm, setSettingsForm] = useState<{ const [settingsForm, setSettingsForm] = useState<{
@@ -372,6 +374,9 @@ export default function AdminWheel() {
mutationFn: adminWheelApi.reorderPrizes, mutationFn: adminWheelApi.reorderPrizes,
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['admin-wheel-config'] }); queryClient.invalidateQueries({ queryKey: ['admin-wheel-config'] });
// Reset local state after successful save
setLocalPrizeOrder(null);
setHasUnsavedOrder(false);
}, },
}); });
@@ -426,13 +431,38 @@ export default function AdminWheel() {
const newIndex = config.prizes.findIndex((p) => p.id === over.id); const newIndex = config.prizes.findIndex((p) => p.id === over.id);
if (oldIndex !== -1 && newIndex !== -1) { if (oldIndex !== -1 && newIndex !== -1) {
const newOrder = arrayMove(config.prizes, oldIndex, newIndex); const newOrder = arrayMove(config.prizes, oldIndex, newIndex);
reorderPrizesMutation.mutate(newOrder.map((p) => p.id)); // Save order locally, don't trigger mutation immediately
setLocalPrizeOrder(newOrder.map((p) => p.id));
setHasUnsavedOrder(true);
} }
} }
}, },
[config, onTelegramDragEnd, reorderPrizesMutation], [config, onTelegramDragEnd],
); );
// Save prize order
const handleSavePrizeOrder = useCallback(() => {
if (localPrizeOrder) {
reorderPrizesMutation.mutate(localPrizeOrder);
setHasUnsavedOrder(false);
}
}, [localPrizeOrder, reorderPrizesMutation]);
// Discard order changes
const handleDiscardOrderChanges = useCallback(() => {
setLocalPrizeOrder(null);
setHasUnsavedOrder(false);
}, []);
// Get prizes in display order (use local order if available)
const displayedPrizes = config?.prizes
? localPrizeOrder
? localPrizeOrder
.map((id) => config.prizes.find((p) => p.id === id))
.filter((p): p is WheelPrizeAdmin => p !== undefined)
: config.prizes
: [];
if (isLoading) { if (isLoading) {
return ( return (
<div className="flex min-h-[400px] items-center justify-center"> <div className="flex min-h-[400px] items-center justify-center">
@@ -764,6 +794,38 @@ export default function AdminWheel() {
</button> </button>
</div> </div>
{/* Unsaved order changes banner */}
{hasUnsavedOrder && (
<div className="flex items-center gap-3 rounded-xl border border-warning-500/30 bg-warning-500/10 p-4">
<div className="flex-1">
<p className="text-sm font-medium text-warning-400">
{t('admin.wheel.prizes.unsavedOrder') || 'Есть несохраненные изменения порядка'}
</p>
<p className="text-xs text-warning-400/70">
{t('admin.wheel.prizes.unsavedOrderHint') ||
'Сохраните изменения или отмените их'}
</p>
</div>
<div className="flex gap-2">
<button
onClick={handleDiscardOrderChanges}
className="rounded-lg border border-dark-600 bg-dark-700 px-4 py-2 text-sm text-dark-200 transition-colors hover:bg-dark-600"
>
{t('common.cancel') || 'Отменить'}
</button>
<button
onClick={handleSavePrizeOrder}
disabled={reorderPrizesMutation.isPending}
className="rounded-lg bg-warning-500 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-warning-600 disabled:opacity-50"
>
{reorderPrizesMutation.isPending
? t('common.saving') || 'Сохранение...'
: t('common.save') || 'Сохранить'}
</button>
</div>
</div>
)}
{/* Create new prize inline form */} {/* Create new prize inline form */}
{isCreating && ( {isCreating && (
<InlinePrizeForm <InlinePrizeForm
@@ -785,11 +847,11 @@ export default function AdminWheel() {
onDragCancel={onTelegramDragCancel} onDragCancel={onTelegramDragCancel}
> >
<SortableContext <SortableContext
items={config.prizes.map((p) => p.id)} items={displayedPrizes.map((p) => p.id)}
strategy={verticalListSortingStrategy} strategy={verticalListSortingStrategy}
> >
<div className="space-y-3"> <div className="space-y-3">
{config.prizes.map((prize) => ( {displayedPrizes.map((prize) => (
<SortablePrizeCard <SortablePrizeCard
key={prize.id} key={prize.id}
prize={prize} prize={prize}