|
|
@@ -1,6 +1,6 @@
|
|
|
'use client'
|
|
|
|
|
|
-import { useState } from 'react'
|
|
|
+import { useCallback, useEffect, useRef, useState } from 'react'
|
|
|
import { useTranslations } from 'next-intl'
|
|
|
import { Input } from '@/components/ui/input'
|
|
|
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'
|
|
|
@@ -9,6 +9,8 @@ import { formatCurrency } from '@/lib/format'
|
|
|
import type { ServicePartInput } from '@/features/vehicles/Schema/serviceSchema'
|
|
|
import type { InventoryPartOption } from './form-types'
|
|
|
|
|
|
+const PAGE_SIZE = 100
|
|
|
+
|
|
|
interface InventoryPickerDialogProps {
|
|
|
open: boolean
|
|
|
onOpenChange: (open: boolean) => void
|
|
|
@@ -26,6 +28,8 @@ export function InventoryPickerDialog({
|
|
|
}: InventoryPickerDialogProps) {
|
|
|
const t = useTranslations('service.parts')
|
|
|
const [search, setSearch] = useState('')
|
|
|
+ const [visibleCount, setVisibleCount] = useState(PAGE_SIZE)
|
|
|
+ const listRef = useRef<HTMLDivElement>(null)
|
|
|
|
|
|
const filtered = inventoryParts.filter((p) => {
|
|
|
if (!search) return true
|
|
|
@@ -37,6 +41,24 @@ export function InventoryPickerDialog({
|
|
|
)
|
|
|
})
|
|
|
|
|
|
+ const visible = filtered.slice(0, visibleCount)
|
|
|
+
|
|
|
+ // Reset visible count when search changes or dialog opens
|
|
|
+ useEffect(() => {
|
|
|
+ setVisibleCount(PAGE_SIZE)
|
|
|
+ }, [search, open])
|
|
|
+
|
|
|
+ const handleScroll = useCallback(() => {
|
|
|
+ const el = listRef.current
|
|
|
+ if (!el) return
|
|
|
+ if (el.scrollTop + el.clientHeight >= el.scrollHeight - 50) {
|
|
|
+ setVisibleCount((prev) => {
|
|
|
+ if (prev >= filtered.length) return prev
|
|
|
+ return prev + PAGE_SIZE
|
|
|
+ })
|
|
|
+ }
|
|
|
+ }, [filtered.length])
|
|
|
+
|
|
|
return (
|
|
|
<Dialog
|
|
|
open={open}
|
|
|
@@ -45,7 +67,7 @@ export function InventoryPickerDialog({
|
|
|
if (!v) setSearch('')
|
|
|
}}
|
|
|
>
|
|
|
- <DialogContent className="sm:max-w-md">
|
|
|
+ <DialogContent className="sm:max-w-2xl">
|
|
|
<DialogHeader>
|
|
|
<DialogTitle>{t('inventoryTitle')}</DialogTitle>
|
|
|
</DialogHeader>
|
|
|
@@ -58,14 +80,14 @@ export function InventoryPickerDialog({
|
|
|
className="pl-9"
|
|
|
/>
|
|
|
</div>
|
|
|
- <div className="max-h-75 overflow-y-auto space-y-1">
|
|
|
- {filtered.map((ip) => (
|
|
|
+ <div ref={listRef} onScroll={handleScroll} className="max-h-75 overflow-y-auto space-y-1">
|
|
|
+ {visible.map((ip) => (
|
|
|
<button
|
|
|
key={ip.id}
|
|
|
type="button"
|
|
|
- className="w-full text-left rounded-md p-2.5 hover:bg-accent transition-colors"
|
|
|
+ className="w-full text-left rounded-md px-2.5 py-1.5 hover:bg-accent transition-colors flex items-center justify-between gap-4"
|
|
|
onClick={() => {
|
|
|
- const price = ip.sellPrice > 0 ? ip.sellPrice : ip.unitCost;
|
|
|
+ const price = ip.sellPrice > 0 ? ip.sellPrice : ip.unitCost
|
|
|
onSelectPart({
|
|
|
partNumber: ip.partNumber || '',
|
|
|
name: ip.name,
|
|
|
@@ -77,34 +99,41 @@ export function InventoryPickerDialog({
|
|
|
onOpenChange(false)
|
|
|
}}
|
|
|
>
|
|
|
- <div className="flex items-center justify-between">
|
|
|
- <div>
|
|
|
- <span className="font-medium text-sm">{ip.name}</span>
|
|
|
+ <div className="min-w-0">
|
|
|
+ <div className="flex items-center gap-2">
|
|
|
+ <span className="font-medium text-sm truncate">{ip.name}</span>
|
|
|
{ip.partNumber && (
|
|
|
- <span className="ml-2 text-xs font-mono text-muted-foreground">
|
|
|
+ <span className="text-xs font-mono text-muted-foreground shrink-0">
|
|
|
{ip.partNumber}
|
|
|
</span>
|
|
|
)}
|
|
|
+ {ip.category && (
|
|
|
+ <span className="text-xs text-muted-foreground shrink-0">{ip.category}</span>
|
|
|
+ )}
|
|
|
</div>
|
|
|
- <span className="text-xs text-muted-foreground">{t('inStock', { quantity: ip.quantity })}</span>
|
|
|
+ {ip.description && (
|
|
|
+ <p className="text-xs text-muted-foreground truncate">{ip.description}</p>
|
|
|
+ )}
|
|
|
</div>
|
|
|
- <div className="flex items-center gap-2 text-xs text-muted-foreground mt-0.5">
|
|
|
- {ip.category && <span>{ip.category}</span>}
|
|
|
+ <div className="flex items-center gap-3 shrink-0 text-xs text-muted-foreground">
|
|
|
{ip.sellPrice > 0 ? (
|
|
|
<>
|
|
|
- <span className="font-medium text-foreground">{formatCurrency(ip.sellPrice, currencyCode)}</span>
|
|
|
- <span className="line-through">{formatCurrency(ip.unitCost, currencyCode)}</span>
|
|
|
+ <span className="font-medium text-foreground">
|
|
|
+ {formatCurrency(ip.sellPrice, currencyCode)}
|
|
|
+ </span>
|
|
|
+ <span className="line-through">
|
|
|
+ {formatCurrency(ip.unitCost, currencyCode)}
|
|
|
+ </span>
|
|
|
</>
|
|
|
) : (
|
|
|
<span>{formatCurrency(ip.unitCost, currencyCode)}</span>
|
|
|
)}
|
|
|
+ <span>{t('inStock', { quantity: ip.quantity })}</span>
|
|
|
</div>
|
|
|
</button>
|
|
|
))}
|
|
|
{filtered.length === 0 && (
|
|
|
- <p className="py-6 text-center text-sm text-muted-foreground">
|
|
|
- {t('noInventory')}
|
|
|
- </p>
|
|
|
+ <p className="py-6 text-center text-sm text-muted-foreground">{t('noInventory')}</p>
|
|
|
)}
|
|
|
</div>
|
|
|
</DialogContent>
|