freshyo/apps/admin-web/src/routes/dashboard.customize-app.ordering.tsx
2026-09-06 23:05:20 +05:30

301 lines
10 KiB
TypeScript

import React, { useState, useEffect, useCallback } from 'react'
import { createFileRoute } from '@tanstack/react-router'
import { useQueryClient } from '@tanstack/react-query'
import { DndContext, closestCenter, PointerSensor, useSensor, useSensors, type DragEndEvent } from '@dnd-kit/core'
import { SortableContext, useSortable, arrayMove, verticalListSortingStrategy } from '@dnd-kit/sortable'
import { CSS } from '@dnd-kit/utilities'
import { GripVertical, Image as ImageIcon, XCircle, ChevronLeft, CircleAlert, Package } from 'lucide-react'
import { AppContainer, p as MyText } from 'web-components'
import { trpc } from '@/lib/trpc-client'
import type { StoreProductCard } from '@packages/shared'
import type { DragOrderItemProps } from '@/types/drag-order-item'
export const Route = createFileRoute('/dashboard/customize-app/ordering')({
component: AllItemsOrder,
})
type Product = Pick<StoreProductCard, 'id' | 'name' | 'images' | 'isOutOfStock'>
const itemStyle: React.CSSProperties = {
width: '100%',
height: 60,
backgroundColor: 'white',
borderRadius: 8,
borderWidth: 1,
borderStyle: 'solid',
borderColor: '#e5e7eb',
padding: 10,
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
boxShadow: '0 1px 2px rgba(0,0,0,0.1)',
marginTop: 4,
marginBottom: 4,
}
const activeItemStyle: React.CSSProperties = {
...itemStyle,
borderColor: '#3b82f6',
boxShadow: '0 4px 8px rgba(59,130,246,0.3)',
}
const ProductItem: React.FC<DragOrderItemProps<Product>> = ({
item,
isActive,
}) => {
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id: item.id })
const active = isActive || isDragging
return (
<div
ref={setNodeRef}
style={{
...(active ? activeItemStyle : itemStyle),
transform: CSS.Translate.toString(transform),
transition,
opacity: item.isOutOfStock ? 0.6 : 1,
zIndex: isDragging ? 50 : undefined,
}}
>
<div {...attributes} {...listeners} style={{ marginRight: 8, padding: 2, cursor: 'grab', touchAction: 'none' }}>
<GripVertical
size={24}
color={active ? '#3b82f6' : '#9ca3af'}
/>
</div>
{item.images?.[0] ? (
<img
src={item.images[0]}
style={{ width: 30, height: 30, borderRadius: 6, marginRight: 10, objectFit: 'cover' }}
alt={item.name}
/>
) : (
<div style={{ width: 30, height: 30, borderRadius: 6, backgroundColor: '#f3f4f6', marginRight: 10, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<ImageIcon size={24} color="#9ca3af" />
</div>
)}
<div style={{ flex: 1, display: 'flex', flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' }}>
<MyText style={{ fontSize: 13, color: '#111827', fontWeight: 500, flex: 1, marginRight: 4 }} numberOfLines={1}>
{item.name.length > 30 ? item.name.substring(0, 30) + '...' : item.name}
</MyText>
{item.isOutOfStock && (
<XCircle size={16} color="#dc2626" />
)}
</div>
</div>
)
}
function AllItemsOrder() {
const queryClient = useQueryClient()
const [products, setProducts] = useState<Product[]>([])
const [hasChanges, setHasChanges] = useState(false)
const { data: constants, isLoading: isLoadingConstants, error: constantsError } = trpc.admin.const.getConstants.useQuery()
const { data: allProducts, isLoading: isLoadingProducts, error: productsError } = trpc.common.product.getAllProductsSummary.useQuery()
const updateConstants = trpc.admin.const.updateConstants.useMutation()
const sensors = useSensors(useSensor(PointerSensor, { activationConstraint: { distance: 8 } }))
useEffect(() => {
if (allProducts?.products) {
const allItemsOrderConstant = constants?.find((c) => c.key === 'allItemsOrder')
let orderedIds: number[] = []
if (allItemsOrderConstant) {
const value = allItemsOrderConstant.value
if (Array.isArray(value)) {
orderedIds = value.map((id: any) => parseInt(id))
} else if (typeof value === 'string') {
orderedIds = value.split(',').map((id: string) => parseInt(id.trim())).filter((id) => !isNaN(id))
}
}
const productMap = new Map(allProducts.products.map((p) => [p.id, p]))
const sortedProducts: Product[] = []
for (const id of orderedIds) {
const product = productMap.get(id)
if (product) {
sortedProducts.push({
id: product.id,
name: product.name,
images: product.images || [],
isOutOfStock: product.isOutOfStock || false,
})
productMap.delete(id)
}
}
for (const product of productMap.values()) {
sortedProducts.push({
id: product.id,
name: product.name,
images: product.images || [],
isOutOfStock: product.isOutOfStock || false,
})
}
setProducts(sortedProducts)
}
}, [constants, allProducts])
const handleDragEnd = useCallback((event: DragEndEvent) => {
const { active, over } = event
if (over && active.id !== over.id) {
setProducts((prev) => {
const oldIndex = prev.findIndex((p) => p.id === active.id)
const newIndex = prev.findIndex((p) => p.id === over.id)
return arrayMove(prev, oldIndex, newIndex)
})
setHasChanges(true)
}
}, [])
const handleSave = () => {
const productIds = products.map((p) => p.id)
updateConstants.mutate(
{
constants: [{
key: 'allItemsOrder',
value: productIds,
}],
},
{
onSuccess: () => {
setHasChanges(false)
window.alert('All items order updated successfully!')
queryClient.invalidateQueries({ queryKey: ['const.getConstants'] })
},
onError: (error) => {
window.alert('Failed to update items order. Please try again.')
console.error('Update all items order error:', error)
},
}
)
}
if (isLoadingConstants || isLoadingProducts) {
return (
<AppContainer>
<div className="flex-1 bg-gray-50">
<div className="flex flex-row items-center justify-between border-b border-gray-200 bg-white px-4 py-4">
<button
onClick={() => window.history.back()}
className="-ml-4 p-2"
>
<ChevronLeft size={24} color="#374151" />
</button>
<MyText className="text-xl font-bold text-gray-900">All Items Order</MyText>
<div className="w-16" />
</div>
<div className="flex flex-1 flex-col items-center justify-center p-8">
<div className="h-8 w-8 animate-spin rounded-full border-2 border-gray-300 border-t-blue-500" />
<MyText className="mt-4 text-center text-gray-500">
{isLoadingConstants ? 'Loading order...' : 'Loading products...'}
</MyText>
</div>
</div>
</AppContainer>
)
}
if (constantsError || productsError) {
return (
<AppContainer>
<div className="flex-1 bg-gray-50">
<div className="flex flex-row items-center justify-between border-b border-gray-200 bg-white px-4 py-4">
<button
onClick={() => window.history.back()}
className="-ml-4 p-2"
>
<ChevronLeft size={24} color="#374151" />
</button>
<MyText className="text-xl font-bold text-gray-900">All Items Order</MyText>
<div className="w-16" />
</div>
<div className="flex flex-1 flex-col items-center justify-center p-8">
<CircleAlert size={64} color="#ef4444" />
<MyText className="mt-4 text-lg font-bold text-gray-900">Error</MyText>
<MyText className="mt-2 text-center text-gray-500">
{constantsError ? 'Failed to load order' : 'Failed to load products'}
</MyText>
<button
onClick={() => window.history.back()}
className="mt-6 rounded-full bg-blue-600 px-6 py-3"
>
<MyText className="font-semibold text-white">Go Back</MyText>
</button>
</div>
</div>
</AppContainer>
)
}
return (
<div className="flex-1 bg-gray-50">
<div className="flex flex-row items-center justify-between border-b border-gray-200 bg-white px-4 py-4">
<button
onClick={() => window.history.back()}
className="-ml-4 p-2"
>
<ChevronLeft size={24} color="#374151" />
</button>
<MyText className="text-xl font-bold text-gray-900">All Items Order</MyText>
<button
onClick={handleSave}
disabled={!hasChanges || updateConstants.isPending}
className={`rounded-lg px-4 py-2 ${
hasChanges && !updateConstants.isPending
? 'bg-blue-600'
: 'bg-gray-300'
}`}
>
<MyText className={`font-semibold ${
hasChanges && !updateConstants.isPending
? 'text-white'
: 'text-gray-500'
}`}>
{updateConstants.isPending ? 'Saving...' : 'Save'}
</MyText>
</button>
</div>
{products.length === 0 ? (
<div className="flex flex-1 flex-col items-center justify-center p-8">
<Package size={64} color="#e5e7eb" />
<MyText className="mt-4 text-center text-lg text-gray-500">
No products available
</MyText>
</div>
) : (
<div className="flex flex-1 flex-col">
<div className="mx-4 mb-2 mt-2 rounded-lg bg-blue-50 px-4 py-2">
<MyText className="text-center text-xs text-blue-700">
Long press and drag to reorder {products.length} items
</MyText>
</div>
<div className="flex-1 px-3 pb-5">
<DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={handleDragEnd}>
<SortableContext items={products.map((p) => p.id)} strategy={verticalListSortingStrategy}>
{products.map((product) => (
<ProductItem key={product.id} item={product} drag={() => {}} isActive={false} />
))}
</SortableContext>
</DndContext>
</div>
</div>
)}
</div>
)
}