1
0
forked from a/lifeto-shop
lifeto-shop/src/components/inventory/MoveConfirmationPopup.tsx

183 lines
6.1 KiB
TypeScript
Raw Normal View History

2025-06-23 06:33:03 +00:00
import {
FloatingFocusManager,
FloatingOverlay,
FloatingPortal,
useClick,
useDismiss,
useFloating,
useInteractions,
useRole,
} from '@floating-ui/react'
import { useAtom, useSetAtom } from 'jotai'
import { useState } from 'react'
import {
clearItemSelectionActionAtom,
closeMoveConfirmationAtom,
type MoveItemsResult,
moveConfirmationAtom,
moveSelectedItemsAtom,
} from '@/state/atoms'
export function MoveConfirmationPopup() {
const [confirmationState] = useAtom(moveConfirmationAtom)
const closeConfirmation = useSetAtom(closeMoveConfirmationAtom)
const moveItems = useSetAtom(moveSelectedItemsAtom)
const clearSelection = useSetAtom(clearItemSelectionActionAtom)
const [isMoving, setIsMoving] = useState(false)
const [moveResult, setMoveResult] = useState<MoveItemsResult | null>(null)
const { refs, context } = useFloating({
open: confirmationState.isOpen,
onOpenChange: open => {
if (!open && !isMoving) {
closeConfirmation()
setMoveResult(null)
}
},
})
const click = useClick(context)
const dismiss = useDismiss(context, {
outsidePressEvent: 'mousedown',
escapeKey: !isMoving,
})
const role = useRole(context)
const { getFloatingProps } = useInteractions([click, dismiss, role])
if (!confirmationState.isOpen) return null
const { selectedItems, sourceCharacter, targetCharacter } = confirmationState
const handleConfirm = async () => {
setIsMoving(true)
try {
const result = await moveItems()
setMoveResult(result)
if (result.failedCount === 0) {
clearSelection()
setTimeout(() => {
closeConfirmation()
setMoveResult(null)
}, 1500)
}
} catch (_error) {
// Error handled in UI
} finally {
setIsMoving(false)
}
}
const handleCancel = () => {
if (!isMoving) {
closeConfirmation()
}
}
const renderItemPreview = () => {
const itemsArray = Array.from(selectedItems.values())
const totalUniqueItems = itemsArray.length
const totalQuantity = itemsArray.reduce((sum, { count }) => sum + count, 0)
if (totalUniqueItems > 5) {
return (
<div className="text-center py-4">
<p className="text-lg font-semibold">Moving {totalUniqueItems} different items</p>
<p className="text-sm text-gray-600">Total quantity: {totalQuantity.toLocaleString()}</p>
</div>
)
}
return (
<div className="space-y-2 max-h-60 overflow-y-auto">
{itemsArray.map(({ item, count }) => (
<div
key={item.id}
className="flex items-center justify-between px-2 py-1 hover:bg-gray-50 rounded"
>
<div className="flex items-center gap-2">
<img
src={item.item_image || ''}
alt={item.item_name}
className="w-6 h-6 object-contain"
/>
<span className="text-sm">{item.item_name}</span>
</div>
<span className="text-sm font-medium text-gray-600">×{count.toLocaleString()}</span>
</div>
))}
</div>
)
}
return (
<FloatingPortal>
<FloatingOverlay className="grid place-items-center bg-black/50 z-50" lockScroll>
<FloatingFocusManager context={context} initialFocus={-1}>
<div
ref={refs.setFloating}
className="bg-white rounded-lg shadow-xl border border-gray-200 p-6 max-w-md w-full mx-4"
{...getFloatingProps()}
>
<h2 className="text-xl font-bold mb-4">Confirm Item Movement</h2>
<div className="mb-4 space-y-2">
<div className="flex items-center gap-2">
<span className="text-sm text-gray-600">From:</span>
<span className="font-medium">{sourceCharacter?.name || 'Unknown'}</span>
</div>
<div className="flex items-center gap-2">
<span className="text-sm text-gray-600">To:</span>
<span className="font-medium">{targetCharacter?.name || 'Unknown'}</span>
</div>
</div>
<div className="border-t border-b border-gray-200 py-4 mb-4">{renderItemPreview()}</div>
{moveResult && (
<div
className={`mb-4 p-3 rounded ${moveResult.failedCount > 0 ? 'bg-yellow-50' : 'bg-green-50'}`}
>
<p className="text-sm font-medium">
{moveResult.failedCount === 0
? `Successfully moved ${moveResult.successCount} items!`
: `Moved ${moveResult.successCount} of ${moveResult.totalItems} items`}
</p>
{moveResult.errors.length > 0 && (
<div className="mt-2 text-xs text-red-600">
{moveResult.errors.slice(0, 3).map(error => (
<p key={error.itemId}>{error.error}</p>
))}
{moveResult.errors.length > 3 && (
<p>...and {moveResult.errors.length - 3} more errors</p>
)}
</div>
)}
</div>
)}
<div className="flex gap-3 justify-end">
<button
type="button"
onClick={handleCancel}
disabled={isMoving}
className="px-4 py-2 text-sm font-medium text-gray-700 bg-gray-100 hover:bg-gray-200 rounded-md transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
Cancel
</button>
<button
type="button"
onClick={handleConfirm}
disabled={isMoving || moveResult !== null}
className="px-4 py-2 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 rounded-md transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
{isMoving ? 'Moving...' : 'Confirm Move'}
</button>
</div>
</div>
</FloatingFocusManager>
</FloatingOverlay>
</FloatingPortal>
)
}