1
0
forked from a/lifeto-shop
lifeto-shop/src/components/inventory/MoveConfirmationPopup.tsx
2025-06-23 01:33:03 -05:00

183 lines
6.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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>
)
}